const PART_RENDER_WIDTH = 100;
const PART_RENDER_HEIGHT = 50;

const ENTRY_RENDER_DISTANCE = 20;
const ENTRY_EACH_COLUMN = 4;

const ENTRY_RENDER_MARGIN = 20;

const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;

export const SOLVER_FPS = 40;

/**
 * 
 * @param { import("./flowchart").Flowchart } chart 
 * @param { import("./flowchart").Cursor[] } cursors 
 * @returns { import("./flowchart").FlowchartPart }
 */
function getPartAt(chart, cursors) {
    if (Array.isArray(chart)) return getPartAt({
        parts: chart,
    }, cursors);
    let current = undefined;
    try {
        for (const cursor of cursors) {
            const { type, index } = cursor;
            switch (type) {
                case "root":
                    current = chart.parts[index];
                    break;
                case "success":
                    current = current.success[index];
                    break;
                case "fail":
                    current = current.fail[index];
                    break;
            }
            if (!current) {
                throw undefined;
            }
        }
    } catch (e) {
        console.error(e);
        throw new Error(`failed to visit ${cursors.map(cursor => cursor.index.toString()).join(".")}`);
    }
    return current;
}

/**
 * 
 * @param { import("./flowchart").Flowchart } chart 
 * @param { import("./flowchart").Cursor[] } cursors 
 * @param { import("./flowchart").FlowchartPart } part
 */
function setPartAt(chart, cursors, part) {
    if (Array.isArray(chart)) return setPartAt({
        parts: chart,
    }, cursors, part);
    let current = undefined;
    const prefix = cursors.slice(0, -1);
    const target = cursors[cursors.length - 1];
    try {
        for (const cursor of prefix) {
            const { type, index } = cursor;
            switch (type) {
                case "root":
                    current = chart.parts[index];
                    break;
                case "success":
                    current = current.success[index];
                    break;
                case "fail":
                    current = current.fail[index];
                    break;
            }
            if (!current) {
                throw undefined;
            }
        }
        const { type, index } = target;


        /**
         * @type { import("./flowchart").FlowchartPart }
         */
        let rTarget;
        switch (type) {
            case "root":
            rTarget = chart.parts[index];
                break;
            case "success":
                rTarget = current.success[index];
                break;
            case "fail":
                rTarget = current.fail[index];
                break;
        }
        rTarget.block = part.block;
        rTarget.description = part.description;

    } catch (e) {
        console.error(e);
        throw new Error(`failed to visit ${cursors.map(cursor => cursor.index.toString()).join(".")}`);
    }
}

/**
 * @param { import("./flowchart").Flowchart | import("./flowchart").FlowchartPart[] } chart 
 * @param { import("./flowchart").Cursor[] } groupCursors 
 * @returns { import("./flowchart").FlowchartPart[] }
 */
function getPartGroupAt(chart, groupCursors) {
    if (!Array.isArray(chart)) {
        return getPartGroupAt(chart.parts, groupCursors);
    }
    /**
     * @type { import("./flowchart").FlowchartPart[] }
     */
    let current;
    for (const { type, index } of groupCursors) {
        switch (type) {
            case "root":
                current = chart;
                break;
            case "success":
                current = current[index].success;
                break;
            case "fail":
                current = current[index].fail;
                break;
        }
    }
    return current;
}


/**
 * 
 * @param { Array } array 
 */
function createShuffle(array) {
    array = array.slice();
    const result = [];
    while (array.length > 0) {
        const index = Math.floor(Math.random() * array.length);
        result.push(array[index]);
        array.splice(index, 1);
    }
    return result;
}

/**
 * @param { HTMLCanvasElement } canvas
 * @param { import("./flowchart").Flowchart } chart 
 */
export function createFlowchartSolver(canvas, chart) {
    const tagMap = getTagMap(chart);

    return {
        active: true,
        chart,
        canvas,
        minScrollY: 0,
        maxScrollY: 0,
        scrollY: 0,
        canvasRect: canvas.getBoundingClientRect(),
        context: canvas.getContext("2d"),
        /**
         * @type { {[problem in chart.problems]: {attachedTo?: string, x: number, y: number, defaultX: number, defaultY: number, part: import("./flowchart").FlowchartPart} }
         */
        entries: Object.fromEntries(createShuffle(chart.problems).map((p, index) => [p, {
            attachedTo: undefined,
            part: getPartAt(chart, tagMap[p]),
            defaultX: CANVAS_WIDTH - ENTRY_RENDER_MARGIN - Math.floor(index / ENTRY_EACH_COLUMN) * (ENTRY_RENDER_DISTANCE + PART_RENDER_WIDTH) - PART_RENDER_WIDTH,
            defaultY: ENTRY_RENDER_MARGIN + (index % ENTRY_EACH_COLUMN) * (ENTRY_RENDER_DISTANCE + PART_RENDER_HEIGHT),
            x: 0,
            y: 0
        }])),

        tagMap,

        row: 0,
        /**
         * @type { import("./flowchart").RenderPart[] }
         */
        renderParts: [],
        renderChart() {
            this.jump = [];
            this.row = 0;
            this.renderParts = [];
            this._renderChart([
                {
                    "type": "root"
                },
            ], undefined);

            this.jump.forEach(([from, toTag, type]) => {
                const targets = this.renderParts.filter(part => part.part.tag === toTag);
                if (targets.length === 0) return;
                const target = targets[0];
                target.fromParts.push({
                    from,
                    type
                });
            });

            const partsByRow = [];
            this.renderParts.forEach(part => {
                while (partsByRow.length - 1 < part.row) partsByRow.push([]);
                partsByRow[part.row].push(part);
            });
            // positions
            for (const [row, parts] of partsByRow.map(/** @return { [number, import("./flowchart").RenderPart[]] } */(value, index) => [index, value])) {
                const baseY = ENTRY_RENDER_MARGIN + row * (ENTRY_RENDER_MARGIN + PART_RENDER_HEIGHT);
                let x = ENTRY_RENDER_MARGIN;
                for (const [index, part] of parts.map(/** @return { [number, import("./flowchart").RenderPart] } */(value, index) => [index, value])) {

                    if (part.from && part.from.x > x) {
                        x = part.from.x;
                    }
                    part.x = x;
                    part.y = baseY;
                    x += (PART_RENDER_WIDTH + ENTRY_RENDER_MARGIN);
                }
            }
            // render!
            // draw lines and arrows
            for (const part of this.renderParts) {
                //console.log("current part:", part.description);
                for (const fromPart of part.fromParts) {
                    const fromX = fromPart.from.x + PART_RENDER_WIDTH / 2;
                    const fromY = fromPart.from.y + PART_RENDER_HEIGHT / 2;
                    const toX = part.x + PART_RENDER_WIDTH / 2;
                    const toY = part.y + PART_RENDER_HEIGHT / 2;

                    this.context.strokeStyle = "gray";
                    this.context.beginPath();
                    this.context.moveTo(fromX, fromY - this.scrollY);
                    const mX = Math.min(CANVAS_WIDTH - ENTRY_RENDER_MARGIN, fromX + CANVAS_WIDTH / 2);
                    if (fromPart.from.row > part.row) {
                        this.context.lineTo(mX, fromY - this.scrollY);
                        this.context.lineTo(mX, toY - this.scrollY);
                        this.context.lineTo(toX, toY - this.scrollY);
                        this.context.stroke();

                        this.context.fillStyle = "gray";
                        this.context.beginPath();
                        const point0 = [mX, (fromY + toY) / 2 - 30 - this.scrollY];
                        const point1 = [mX - 10, (fromY + toY) / 2 - 30 + 10 - this.scrollY];
                        const point2 = [mX + 10, (fromY + toY) / 2 - 30 + 10 - this.scrollY];

                        this.context.moveTo(...point0);
                        this.context.lineTo(...point1);
                        this.context.lineTo(...point2);
                        this.context.lineTo(...point0);

                        this.context.fill();
                    }
                    else {
                        this.context.lineTo(toX, toY - this.scrollY);
                        this.context.stroke();

                        /*
                        this.context.beginPath();
                        const basePoint1Transform = [-3, -3];
                        const basePoint2Transform = [3, -3];
                        const rotation = Math.atan((toY - fromY)/(toX - fromX)) + (toY - fromY) < 0 ? Math.PI : 0;

                        const point0 = [(fromX + toX) / 2, (fromY + toY) / 2 - this.scrollY];
                        const point1 = [(fromX + toX) / 2 + basePoint1Transform, (fromY + toY) / 2 - 3 - this.scrollY];
                        const point2 = [mX + 3, (fromY + toY) / 2 - 3 - this.scrollY];
                        
                        this.context.moveTo(...point0);
                        this.context.lineTo(...point1);
                        this.context.lineTo(...point2);
                        this.context.lineTo(...point0);
                        this.context.stroke();
                        */
                    }

                    // draw condition text
                    if (fromPart.type !== "normal") {
                        this.context.fillStyle = fromPart.type === "success" ? "green" : "red";
                        this.context.textAlign = "center";
                        this.context.font = "Consolas 20px";
                        if (fromPart.from.row > part.row) {
                            const mX = Math.min(CANVAS_WIDTH - ENTRY_RENDER_MARGIN, fromX + CANVAS_WIDTH / 2);
                            this.context.fillText(fromPart.type === "success" ? "真" : "假", mX, (fromY + toY) / 2 - this.scrollY);
                        } else {
                            this.context.fillText(fromPart.type === "success" ? "真" : "假", (fromX + toX) / 2, (fromY + toY) / 2 - this.scrollY);
                        }
                    }

                }
            }
            this.renderProblemParts = [];
            // draw parts
            for (const part of this.renderParts) {
                this.maxScrollY = Math.max(this.minScrollY, part.y + CANVAS_HEIGHT);

                this.context.strokeStyle = part.problem ? "red" : "black";
                this.context.clearRect(part.x, part.y - this.scrollY, PART_RENDER_WIDTH, PART_RENDER_HEIGHT);
                this.context.strokeRect(part.x, part.y - this.scrollY, PART_RENDER_WIDTH, PART_RENDER_HEIGHT);
                if (!part.problem) {
                    this.context.fillStyle = "black";
                    this.context.textAlign = "center";
                    this.context.font = "Consolas 30px";
                    this.context.fillText(part.description, part.x + PART_RENDER_WIDTH / 2, part.y + PART_RENDER_HEIGHT / 2 - this.scrollY);
                } else {
                    this.renderProblemParts.push(part);
                }
            }

        },
        /**
         * @type { import("./flowchart").RenderPart[] }
         */
        renderProblemParts: [],
        /**
         * @type { [import("./flowchart").RenderPart, string, "normal" | "success" | "fail"][] }
         */
        jump: [],
        /**
         * @param { import("./flowchart").Cursor[] } groupCursors 
         * @param { import("./flowchart").RenderPart? } parent 
         * @param { "success" | "fail" | undefined } conditionType
         * @returns { import("./flowchart").RenderPart } tail
         */
        _renderChart(groupCursors, parent, conditionType) {
            const group = getPartGroupAt(this.chart, groupCursors);
            /**
             * @type { import("./flowchart").RenderPart }
             */
            let lastPart;
            /**
             * @type { import("./flowchart").RenderPart[] }
             */
            let lastTails = [];
            for (const [index, part] of group.map(/** @returns { [number, import("./flowchart").FlowchartPart] }  */(part, index) => [index, part])) {
                /**
                 * @type { import("./flowchart").RenderPart }
                 */
                const currentPart = {
                    "problem": this.chart.problems.indexOf(part.tag) !== -1,
                    "row": this.row,
                    "fromParts": (lastTails.length !== 0 ? lastTails : (lastPart ? [lastPart] : (parent ? [parent] : []))).map(v => {
                        return {
                            from: v,
                            type: (v.part.type === "condition" && !lastPart && conditionType) ? conditionType : "normal"
                        };
                    }),
                    "from": lastPart || parent || undefined,
                    "fromType": parent?.part?.type === "condition" && !lastPart && conditionType ? conditionType : "normal",
                    "part": part,
                    "description": part.description
                };
                //console.log(lastTails);
                lastTails = [];
                lastPart = currentPart;
                this.renderParts.push(currentPart);
                this.row += 1;
                if (Array.isArray(part.success)) {
                    const currentRow = this.row;
                    lastTails.push(...this._renderChart([...groupCursors, {
                        "type": "success",
                        index
                    }], currentPart, "success"));
                    const successRow = this.row;

                    this.row = currentRow;
                    Array.isArray(part.fail) && lastTails.push(...this._renderChart([...groupCursors, {
                        "type": "fail",
                        index
                    }], currentPart, "fail"));
                    const failRow = this.row;

                    this.row = Math.max(successRow, failRow);
                }
                else {
                    part.success && this.jump.push([currentPart, part.success, "success"]);
                    part.fail && this.jump.push([currentPart, part.fail, "fail"]);
                }
            }
            return lastTails.length === 0 ? [lastPart] : lastTails;
        },

        scrollDown() {
            this.scrollY += PART_RENDER_HEIGHT * 5 / SOLVER_FPS;
        },

        scrollUp() {
            this.scrollY -= PART_RENDER_HEIGHT * 5 / SOLVER_FPS;
        },

        manageScroll() {
            this.scrollY = Math.max(Math.min(this.scrollY, this.maxScrollY), this.minScrollY);
        },

        canvasFocused: false,
        startListenMouse() {
            this.canvas.addEventListener("focus", () => this.canvasFocused = true);
            this.canvas.addEventListener("blur", () => this.canvasFocused = false);

            this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this));
            this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
            this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
            this.canvas.addEventListener("wheel", this.onMouseScroll.bind(this));
        },

        dragging: false,
        /**
         * @type { string }
         */
        dragTarget: undefined,
        /**
         * 
         * @param { MouseEvent } ev 
         */
        getMousePosition(ev) {
            this.canvasRect = this.canvas.getBoundingClientRect();
            return [ev.clientX - this.canvasRect.left, ev.clientY - this.canvasRect.top];
        },
        lastMouseY: undefined,
        /**
         * 
         * @param { MouseEvent } ev 
         */
        onMouseMove(ev) {
            if (!this.canvasFocused) return;
            if (this.dragging) {
                const [x, y] = this.getMousePosition(ev);
                if (this.dragTarget) {
                    this.entries[this.dragTarget].x = x - PART_RENDER_WIDTH / 2;
                    this.entries[this.dragTarget].y = y - PART_RENDER_HEIGHT / 2 + this.scrollY;
                }
                else {
                    return;
                    if (!this.lastMouseY) return this.lastMouseY = y;
                    if (this.lastMouseY < y) this.scrollUp();
                    if (this.lastMouseY > y) this.scrollDown();
                }
            }
        },

        /**
         * 
         * @param { MouseEvent } ev 
         */
        onMouseUp(ev) {
            if (this.dragging) {
                if (this.dragTarget) {
                    const dragTarget = this.entries[this.dragTarget];
                    const collideProblems = this.renderProblemParts
                        .filter(part => !(dragTarget.x + PART_RENDER_WIDTH < part.x || dragTarget.x > part.x + PART_RENDER_WIDTH || dragTarget.y + PART_RENDER_HEIGHT < part.y || dragTarget.y > part.y + PART_RENDER_HEIGHT))
                        .sort((a, b) => Math.sqrt((a.x - dragTarget.x) ^ 2 + (a.y - dragTarget.y) ^ 2) - Math.sqrt((b.x - dragTarget.x) ^ 2 + (b.y - dragTarget.y) ^ 2));
                    if (collideProblems.length > 0) {
                        Object.entries(this.entries).forEach(([tagName, entry]) => {
                            if (entry === dragTarget) return;
                            if (entry.attachedTo === collideProblems[0].part.tag) entry.attachedTo = undefined;
                        });

                        dragTarget.attachedTo = collideProblems[0].part.tag;
                        dragTarget.x = collideProblems[0].x;
                        dragTarget.y = collideProblems[0].y;
                    } else {
                        dragTarget.attachedTo = undefined;
                    }
                    this.dragTarget = undefined;
                }
                this.dragging = false;
            }
            if (!this.canvasFocused) return;
        },

        /**
         * 
         * @param { WheelEvent } ev 
         */
        onMouseScroll(ev) {
            if (!this.canvasFocused) return;
            ev.preventDefault();
            if (ev.deltaY < 0) {
                this.scrollDown();
            } if (ev.deltaY > 0) {
                this.scrollUp();
            }
        },

        /**
         * 
         * @param { MouseEvent } ev 
       */
        onMouseDown(ev) {
            if (!this.canvasFocused) return;
            const [x, y] = this.getMousePosition(ev);
            const parts = Object.entries(this.entries).filter(([tagName, entry]) => {
                return entry.x <= x && entry.x + PART_RENDER_WIDTH >= x && (entry.y - scrollY) <= y && (entry.y + PART_RENDER_HEIGHT - this.scrollY) >= y;
            });
            this.dragging = true;
            if (parts.length === 0) return;
            const [tagName, entry] = parts[0];
            this.dragTarget = tagName;
        },

        /**
         * 
         * @returns { import("./flowchart").Flowchart }
         */
        build() {
            const raw = JSON.stringify(this.chart.parts);
            const templateParts = JSON.parse(raw);
            const parts = JSON.parse(raw);
            for (const [tagName, { attachedTo, x, y }] of Object.entries(this.entries)) {
                if (!attachedTo) throw new Error(`${tagName} not attached!`);
                if (!this.tagMap[tagName]) throw new Error(`part ${tagName} not found!`);
                if (!this.tagMap[attachedTo]) throw new Error(`part ${attachedTo} not found!`);
                const partData = getPartAt(templateParts, this.tagMap[tagName]);
                setPartAt(parts, this.tagMap[attachedTo], partData);
            }
            return Object.assign(Object.assign({}, this.chart), { parts });
        },

        async tick() {
            if (!this.active) return;
            this.context.fillStyle = "white";
            this.context.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
            this.renderChart();

            const entries = Object.entries(this.entries).sort(() => 0.5 - Math.random());
            if (!this.dragging) {
                entries.filter(([tagName, entry]) => !entry.attachedTo).forEach(([tagName, entry]) => {
                    entry.x = entry.defaultX;
                    entry.y = entry.defaultY;
                });
            }
            const scrollY = this.scrollY;
            entries.forEach(([tagName, entry]) => {
                // console.log(entry);
                this.maxScrollY = entry.y + CANVAS_HEIGHT;
                this.context.strokeStyle = entry.attachedTo ? "cyan" : "red";
                this.context.strokeRect(entry.x, entry.y - scrollY, PART_RENDER_WIDTH, PART_RENDER_HEIGHT);
                this.context.fillStyle = "black";
                this.context.font = "Consolas 30px";
                this.context.textAlign = "center";
                this.context.fillText(entry.part.description, entry.x + PART_RENDER_WIDTH / 2, entry.y - scrollY + PART_RENDER_HEIGHT / 2);
            });

            this.manageScroll();
        },

        /**
         * 
         * @param { any } env 
         * @param { number? } timeout 
         */
        async run(env, timeout) {
            /**
             * @type { import("./flowchart").Flowchart }
             */
            let playerSolution;
            try {
                playerSolution = this.build();
            } catch (e) {
                throw {
                    id: "incomplete_chart",
                    error: e
                };
            }
            try {
                await playerSolution.run({
                    env: env ?? {},
                    timeout,
                    debug: true
                });
            } catch (e) {
                throw {
                    id: e.message === "execution_timeout" ? "timeout" : "execution_fail",
                    error: e
                }
            }
        }
    };
}

/**
 * 
 * @param { import("./flowchart").Cursor[] } groupCursors
 * @param { number } index  
 * @return { import("./flowchart").Cursor[] }
 */
function fromGroupCursorsToCursors(groupCursors, index) {
    let iterateIndex = 0;
    const result = [];
    while (iterateIndex < groupCursors.length) {
        const nextIndex = groupCursors[iterateIndex + 1] ? groupCursors[iterateIndex + 1].index : index;
        result.push(Object.assign({}, {
            type: groupCursors[iterateIndex].type,
            index: nextIndex
        }));
        iterateIndex++;
    }
    return result;
}

/**
 * @param { import("./flowchart").Flowchart | import("./flowchart").FlowchartPart[] } chart
 * @param { (import("./flowchart").Cursor[])? } groupCursors 
 * @param {boolean} [debug=false] 
 * @returns { Record<string, import("./flowchart").Cursor[]> } 
 */
export function getTagMap(chart, groupCursors) {
    if (Array.isArray(chart)) return getTagMap({ parts: chart }, groupCursors);
    if (!groupCursors) groupCursors = [{ "type": "root" }];
    /**
     * @type { ReturnType<getTagMap> }
     */
    const result = {};
    /**
     * @type { import("./flowchart").FlowchartPart[] }
     */
    let current = undefined;
    try {
        for (const cursor of groupCursors) {
            const { index, type } = cursor;
            switch (type) {
                case "root":
                    current = chart.parts;
                    break;
                case "success":
                    current = current[index].success;
                    break;
                case "fail":
                    current = current[index].fail;
                    break;
            }
        }
        if (!current) throw undefined;
    } catch (e) {
        console.log(e);
        throw new Error(`failed to visit ${groupCursors.map(cursor => cursor.index).join(".")}`);
    }
    for (const [index, part] of current.map((v, index) => [index, v])) {
        if (part.tag) {
            result[part.tag] = fromGroupCursorsToCursors(groupCursors, index, part.tag);
        }

        if (Array.isArray(part.success)) {
            const part = getTagMap(chart, [...groupCursors, { "type": "success", index }]);
            Object.entries(part).forEach(([k, v]) => result[k] = v);
        }
        if (Array.isArray(part.fail)) {
            const part = getTagMap(chart, [...groupCursors, { "type": "fail", index }]);
            Object.entries(part).forEach(([k, v]) => result[k] = v);
        }
    }
    return result;
}

/**
 * @param { number } ms 
 * @returns { Promise<void> }
 */
function sleep(ms) {
    return new Promise((resolve, reject) =>
        setTimeout(resolve, ms));
}

/**
 * 
 * @param { string } url
 * @param { boolean } debug 
 * @returns { import("./flowchart").Flowchart } 
 */
export async function loadFlowchart(url) {
    const raw = await (await fetch(url)).json();
    /**
     * @type { import("./flowchart").Flowchart }
     */
    const obj = Object.assign(raw, {
        blocks: (await import(raw.blocks)).default,
    });
    obj.run = async function ({ env = {}, timeout = 30000, debug = false }) {
        console.log("env:", env);
        console.log("timeout:", timeout);
        timeout = Date.now() + timeout;
        const tags = getTagMap(this);
        /**
         * @type { import("./flowchart").Cursor[] }
         */
        let cursors = [{
            "type": "root",
            "index": 0
        }];
        /**
         * @type { import("./flowchart").FlowchartPart }
         */
        let current = undefined;
        while (true) {
            if (Date.now() >= timeout) throw new Error("execution_timeout");
            if (cursors.length === 0) break;

            let next = undefined;
            let skip = false;
            let nextPart = true;
            try {
                for (const cursor of cursors) {
                    const { type, index } = cursor;
                    switch (type) {
                        case "root":
                            next = this.parts[index];
                            break;
                        case "success":
                            next = next.success[index];
                            break;
                        case "fail":
                            next = next.fail[index];
                            break;
                    }
                    if (!next) {
                        while (cursors[cursors.length - 1] !== cursor) cursors.pop();
                        cursors.pop();
                        cursors.length >= 1 && cursors[cursors.length - 1].index++;
                        skip = true;
                        break;
                    }
                    current = next;
                }
            } catch (e) {
                console.error(e);
                throw new Error(`failed to visit ${cursors.map(cursor => cursor.index.toString()).join(".")}`);
            }
            if (skip) continue;
            const block = this.blocks[current.block];

            if (current.type === "condition") {
                if (debug && current) {
                    console.log("current part:", current.description ?? current.block ?? cursors.map(cursor => cursor.index).join("."));
                }
                const success = await block(env);
                if (success && current.success) {
                    nextPart = false;

                    if (Array.isArray(current.success)) {
                        cursors.push(
                            {
                                "index": 0,
                                "type": "success"
                            }
                        );
                    } else {
                        if (!tags[current.success]) throw new Error(`can't jump to ${current.success}`);
                        cursors = JSON.parse(JSON.stringify(tags[current.success]));
                    }
                }
                else if (!success && current.fail) {
                    nextPart = false;
                    if (Array.isArray(current.fail)) {
                        cursors.push(
                            {
                                "index": 0,
                                "type": "fail"
                            }
                        );
                    } else {
                        if (!tags[current.fail]) throw new Error(`can't jump to ${current.fail}`);
                        cursors = JSON.parse(JSON.stringify(tags[current.fail]));
                    }
                }
            } else {
                if (debug && current) {
                    console.log("current part:", current.description ?? current.block ?? cursors.map(cursor => cursor.index).join("."));
                }
                if (current.block) {
                    if (!block) throw new Error(`block ${current.block} not found`);
                    await block(env);
                }
            }
            if (nextPart) cursors[cursors.length - 1].index++;
            await sleep(10);
        }

    };
    return obj;
}